///////////////////////////////////////////////////////////////////////////////
//
// interf.c - acquire data for SNS Lab Fis acoustic interference experience
// 
// 1st release: A. Lusiani <a.lusiani@sns.it> April 2003
//
// this program includes parts from poll.c, Part of Comedilib,
// Copyright (c) 1999,2000 David A. Schleef <ds@schleef.org>
// poll.c may be freely modified, distri-buted, and combined with
// other software, as long as proper attribution is given in the
// source code.
//

///////////////////////////////////////////////////////////////////////////////
//
// Modifications:
//
//

///////////////////////////////////////////////////////////////////////////////
//
// headers
//

#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <comedilib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <getopt.h>
#include <ctype.h>
#include <string.h>
#include <sys/time.h>
#include <math.h>

///////////////////////////////////////////////////////////////////////////////
//
// function declarations
//

void dump_cmd(FILE *out,comedi_cmd *cmd);
void prepare_nonblocking_input(FILE* input);
void prepare_ai_scan(comedi_t *cifh,
		     int subdev,
		     comedi_cmd *cmdp,
		     unsigned int triggerType,
		     unsigned int sampleRate,
		     unsigned int scanType,
		     unsigned int scanNum,
		     unsigned int *chanList,
		     unsigned int chanListLen
		     );
void do_ai_scan(comedi_t *dev, int subdev, comedi_cmd *cmd);

///////////////////////////////////////////////////////////////////////////////
//
// variable  declarations
//

const char* 		COMEDI_DEV = "/dev/comedi0";
//--- comedi sub-devices for Analog Input and Output
int			AI_SUBDEV;
int			AO_SUBDEV;
//--- comedi sub-devices for Digital Output
int			DO_SUBDEV;

//--- buffer size for streaming data out of the driver
const unsigned int	COMEDI_BUFSIZ =	(4*1024*1024);

//--- Analog Input definitions
const unsigned int 	MIC_AI_CHAN=0;
const unsigned int 	OSC_AI_CHAN=1;
const unsigned int 	TEMP_AI_CHAN=2;

//--- analog input channel list for DAQ (only microphone)
#define AISCAN_BEG	(0)
#define AISCAN_NUM	(1)
unsigned int aiScanList[AISCAN_NUM];

//--- Analog Input voltage range
const float 		AI_VMIN = -10;
const float 		AI_VMAX = 10;

//--- select analog in voltage reference from GROUND
const unsigned int	AI_AREF = AREF_GROUND;

//--- rate at which the input is sampled, 100 kHz
const double		AISCAN_RATE = 200000;
#define 		AISCAN_SAMPLES (4000)

//--- Analog Out for amplitude modulation
const unsigned int 	AM_AO_CHAN = 0;

//--- minimum AO AM value
double 			AM_MIN = -5;
//--- maximum AO AM value
double			AM_MAX = 0;

//--- oscillator on  AM value
double 			AM_OSC_ON = -4.2;
//--- oscillator off AM value
double			AM_OSC_OFF = 0;

//--- select Analog Output voltage reference from GROUND
const unsigned int	AO_AREF = AREF_GROUND;

//--- buffer size where reading from comedi device
#define BUFSIZE		(AISCAN_SAMPLES*AISCAN_NUM)
//--- buffer for ni_pcimio Analog Input driver (16-bit unsigned samples)
sampl_t buf[BUFSIZE];

///////////////////////////////////////////////////////////////////////////////
//
// code
//

int main(int argc, char *argv[])
{
  comedi_t *cifh;
  comedi_cmd cmd;
  int i;
  int rc;
  unsigned long pageSize;
  unsigned int pageNum;
  lsampl_t maxDataAi;
  lsampl_t maxDataAo;
  lsampl_t data;
  double minVoltsAi, maxVoltsAi;
  double minVoltsAo, maxVoltsAo;
  unsigned int rangetAi;
  comedi_range* rangepAi;
  unsigned int rangetAo;
  comedi_range* rangepAo;

  //--- endpoints of acquisition range are valid numbers
  rc = comedi_set_global_oor_behavior(COMEDI_OOR_NUMBER);

  cifh = comedi_open(COMEDI_DEV);
  if(!cifh){
    perror(COMEDI_DEV);
    exit(1);
  }

  //--- get virtual memory page size
  pageSize = sysconf(_SC_PAGE_SIZE);
  //--- get minimum number of pages needed for requested buffer size
  pageNum = (COMEDI_BUFSIZ+1)/pageSize;
  //--- set comedi device streaming buffer size
  rc = comedi_set_buffer_size(cifh, AI_SUBDEV, pageNum*pageSize);
  if (rc<0) {
    fprintf(stderr, "comedi_set_buffer_size: %s\n",strerror(errno));
    exit(1);
  } else {
    fprintf(stderr, "buffer size set to %d\n", rc);
  }

  //--- find which subdevice offers Analog Input
  rc = comedi_find_subdevice_by_type(cifh, COMEDI_SUBD_AI, 0);
  if (rc<0) {
    comedi_perror("comedi_find_device AI");
    exit(1);
  }
  AI_SUBDEV = rc;

  //--- find which subdevice offers Analog Output
  rc = comedi_find_subdevice_by_type(cifh, COMEDI_SUBD_AO, 0);
  if (rc<0) {
    comedi_perror("comedi_find_device AO");
    exit(1);
  }
  AO_SUBDEV = rc;

  //--- find which subdevice offers Digital Output
  rc = comedi_find_subdevice_by_type(cifh, COMEDI_SUBD_DO,0);
  if (rc<0){
    rc = comedi_find_subdevice_by_type(cifh, COMEDI_SUBD_DIO, 0);
    if (rc<0){
      comedi_perror("comedi_find_device DO and DIO");
      exit(1);
    }
    DO_SUBDEV = rc;
    rc = comedi_dio_config(cifh, rc, 0, COMEDI_OUTPUT);
    if (rc<0){
      comedi_perror("comedi_dio_config");
      exit(1);
    }
  } else {
    DO_SUBDEV = rc;
  }
  fprintf(stderr, "Using Digital Output subdevice %d\n", DO_SUBDEV);

  //--- find AI range for 
  rc = comedi_find_range(cifh, AI_SUBDEV,
			 MIC_AI_CHAN, UNIT_volt, AI_VMIN, AI_VMAX);
  if (rc<0) {
    comedi_perror("comedi_find_range");
    exit(1);
  }
  rangetAi = rc;

  //--- get ptr to range struct
  rangepAi = comedi_get_range(cifh, AI_SUBDEV, MIC_AI_CHAN, rangetAi);
  //--- get maximum possible sample for AI
  maxDataAi = comedi_get_maxdata(cifh, AI_SUBDEV, MIC_AI_CHAN);
  minVoltsAi = comedi_to_phys(0, rangepAi, maxDataAi);
  maxVoltsAi = comedi_to_phys(maxDataAi, rangepAi, maxDataAi);

  fprintf(stderr, "Comedi offers AI range n. %d, %f .. %f\n",
	 rangetAi, minVoltsAi, maxVoltsAi);

  //--- build channel list for DAQ scan of microphone
  for(i=0;i<AISCAN_NUM;i++){
    aiScanList[i] = CR_PACK(AISCAN_BEG+i, rangetAi, AI_AREF);
  }

  //--- find AO range for amplitude modulation DAC
  rc = comedi_find_range(cifh, AO_SUBDEV,
			 AM_AO_CHAN, UNIT_volt,
			 AM_MIN, AM_MAX);
  if (rc<0){
    comedi_perror("comedi_find_range");
    exit(1);
  }
  rangetAo = rc;

  //--- get ptr to range struct
  rangepAo = comedi_get_range(cifh, AO_SUBDEV, AM_AO_CHAN, rangetAo);
  //--- get maximum possible sample for AO
  maxDataAo = comedi_get_maxdata(cifh, AO_SUBDEV, AM_AO_CHAN);
  minVoltsAo = comedi_to_phys(0, rangepAo, maxDataAo);
  maxVoltsAo = comedi_to_phys(maxDataAo, rangepAo, maxDataAo);

  fprintf(stderr, "Comedi offers AO range n. %d, %f .. %f\n",
	 rangetAo, minVoltsAo, maxVoltsAo);

  //--- prepare AI scan for microphone input
  prepare_ai_scan(cifh, AI_SUBDEV, &cmd, 
		  TRIG_EXT, AISCAN_RATE, TRIG_COUNT, AISCAN_SAMPLES,
		  aiScanList, AISCAN_NUM);
  
  //--- ask for non-blocking I/O
  fcntl(comedi_fileno(cifh),F_SETFL,O_NONBLOCK);

  //--- prepare non blocking input from stdin for DAQ scan
  // prepare_nonblocking_input(stdin);

  //--- turn oscillator off
  data = comedi_from_phys(AM_OSC_OFF, rangepAo, maxDataAo);
  fprintf(stderr, "Turn oscillator off\n");
  rc = comedi_data_write(cifh, AO_SUBDEV, AM_AO_CHAN,
			 rangetAo, AO_AREF, data);
  if (rc<0){
    comedi_perror("comedi_write_data");
    exit(1);
  }
  //--- wait 0.1 seconds for echoes to fade off
  usleep(100000);

  //--- start comedi Analog Input scan
  rc = comedi_command(cifh, &cmd);
  fprintf(stderr,"Start DAQ, rc= %d\n", rc);
  if (rc<0) {
    fprintf(stderr,"errno=%d\n",errno);
    comedi_perror("comedi_command");
    exit(1);
  }

  //
  // submit instructions to comedi in ordet to:
  // - pulse digital out #0 to trigger DAQ 
  // - start oscillator
  //
  {
    comedi_insn insn[16];
    comedi_insnlist il;
    lsampl_t trigOn[2];
    lsampl_t trigOff[2];
    lsampl_t amOscOn;
    lsampl_t delay;
    unsigned int ilCou = 0;
  
    //--- clear instruction array
    memset(insn,0,sizeof(insn));
	
    //--- setup pointer to instruction array
    il.insns = insn;

    //--- set trigger to 1
    trigOn[0] = 1;
    trigOn[1] = 1;

    //--- set trigger to 0
    trigOff[0] = 1;
    trigOff[1] = 0;
    
    delay = 1000;

    //--- DAC value for enabling oscillator
    amOscOn = comedi_from_phys(AM_OSC_ON, rangepAo, maxDataAo);

    //---- Digital Output #0 to 0
    insn[ilCou].insn = INSN_BITS;
    insn[ilCou].n = 2;
    insn[ilCou].data = &trigOff[0];
    insn[ilCou].subdev = DO_SUBDEV;
    ilCou++;

    //---- wait
    insn[ilCou].insn = INSN_WAIT;
    insn[ilCou].n = 1;
    insn[ilCou].data = &delay;
    ilCou++;

    //---- Digital Output #0 to 1
    insn[ilCou].insn = INSN_BITS;
    insn[ilCou].n = 2;
    insn[ilCou].data = &trigOn[0];
    insn[ilCou].subdev = DO_SUBDEV;
    ilCou++;

    //---- wait
    insn[ilCou].insn = INSN_WAIT;
    insn[ilCou].n = 1;
    insn[ilCou].data = &delay;
    ilCou++;

    //---- Digital Output #0 to 0
    insn[ilCou].insn = INSN_BITS;
    insn[ilCou].n = 2;
    insn[ilCou].data = &trigOff[0];
    insn[ilCou].subdev = DO_SUBDEV;
    ilCou++;

    //---- enable oscillator
    insn[ilCou].insn = INSN_WRITE;
    insn[ilCou].n = 1;
    insn[ilCou].data = &amOscOn;
    insn[ilCou].subdev = AO_SUBDEV;
    insn[ilCou].chanspec = CR_PACK(AM_AO_CHAN, rangetAo, AO_AREF);
    ilCou++;

    //--- setup number of instructions
    il.n_insns = ilCou;

    //--- execute instructions
    rc = comedi_do_insnlist(cifh, &il);
    if (rc < 0){
      comedi_perror("comedi_do_insnlist");
      exit(1);
    }
    
  }

#if 0
  //--- turn oscillator on
  data = comedi_from_phys(AM_OSC_ON, rangepAo, maxDataAo);
  fprintf(stderr, "Turn oscillator on\n");
  rc = comedi_data_write(cifh, AO_SUBDEV, AM_AO_CHAN,
			 rangetAo, AO_AREF, data);
  if (rc<0){
    comedi_perror("comedi_write_data");
    exit(1);
  }
#endif

  do_ai_scan(cifh, AI_SUBDEV, &cmd);

  return 0;
}

//
// prepare_ai_scan
//
// prepare comedi command for Analog Input scan
//
  
void prepare_ai_scan(comedi_t *cifh,
		     int subdev,
		     comedi_cmd *cmdp,
		     unsigned int triggerType,
		     unsigned int sampleRate,
		     unsigned int scanType,
		     unsigned int scanNum,
		     unsigned int *chanList,
		     unsigned int chanListLen
		     )
{
  int rc;

  //--- clear cmd struct
  memset(cmdp, 0, sizeof(*cmdp));
  
  /* the subdevice that the command is sent to */
  cmdp->subdev = 	subdev;
  
  /* flags */
  cmdp->flags =		0;
  
  /* each event requires a trigger, which is specified
     by a source and an argument.  For example, to specify
     an external digital line 3 as a source, you would use
     src=TRIG_EXT and arg=3. */
  
  /* In this case, we specify using TRIG_NOW to start
   * acquisition immediately when the command is issued.  
   * The argument of TRIG_NOW is "number of nsec after
   * NOW", but no driver supports it yet.  Also, no driver
   * currently supports using a start_src other than
   * TRIG_NOW.  */
  cmdp->start_src =		triggerType;
  cmdp->start_arg =		0;
  
  /* The timing of the beginning of each scan is controlled
   * by scan_begin.  TRIG_TIMER specifies that scan_start
   * events occur periodically at a rate of scan_begin_arg
   * nanoseconds between scans. */
  cmdp->scan_begin_src =	TRIG_TIMER;
  cmdp->scan_begin_arg =	((unsigned int)1e9)/sampleRate;
  
  /* The timing between each sample in a scan is controlled
   * by convert.  Like above, TRIG_TIMER specifies that
   * convert events occur periodically at a rate of convert_arg
   * nanoseconds between scans. */
  cmdp->convert_src =	TRIG_TIMER;
  cmdp->convert_arg =	0;
  
  /* The end of each scan is almost always specified using
   * TRIG_COUNT, with the argument being the same as the
   * number of channels in the chanlist.  You could probably
   * find a device that allows something else, but it would
   * be strange. */
  cmdp->scan_end_src =	TRIG_COUNT;
  cmdp->scan_end_arg =	chanListLen;	/* number of channels */
  
  /* The end of acquisition is controlled by stop_src and
   * stop_arg.  The src will typically be TRIG_COUNT or
   * TRIG_NONE.  Specifying TRIG_COUNT will stop acquisition
   * after stop_arg number of scans, or TRIG_NONE will
   * cause acquisition to continue until stopped using
   * comedi_cancel(). */
  cmdp->stop_src =		scanType;
  cmdp->stop_arg =		scanNum;
  
  /* the channel list determined which channels are sampled.
     In general, chanlist_len is the same as scan_end_arg.  Most
     boards require this.  */
  cmdp->chanlist =	chanList;
  cmdp->chanlist_len =	chanListLen;

  fprintf(stderr, "Assembled DAQ command\n");
  dump_cmd(stderr, cmdp);
  rc = comedi_command_test(cifh, cmdp);
  fprintf(stderr, "1st comedi_command_test rc: %d\n",rc);
  if(rc<0){
    fprintf(stderr, "1st comedi_command_test errno: %d\n",errno);
    comedi_perror("comedi_command_test");
    return;
  }
  
  fprintf(stderr, "DAQ command modified by comedi\n");
  dump_cmd(stderr, cmdp);
  rc = comedi_command_test(cifh,cmdp);
  fprintf(stderr,"2nd comedi_command_test rc: %d\n",rc);
  if (rc<0) {
    fprintf(stderr,"2nd comedi_command_test errno: %d\n",errno);
    comedi_perror("comedi_command_test");
    //--- comedi failure, no scan command possible, quit
    exit(1);
  }

  return;
}

//
// prepare_nonblocking_input
//
// setup terminal input for non-blocking input
//

void prepare_nonblocking_input(FILE* input)
{
  struct termios attr;

  //--- if input is a terminal, ask not to wait for newlines, no echo
  if (tcgetattr(fileno(input), &attr) == 0) {
    //--- echo off
    attr.c_lflag &= ~(ECHO);
    //--- do not wait for newline
    attr.c_lflag &= ~(ICANON);
    //--- read at least one char
    attr.c_cc[VMIN] = 1;
    // set new terminal attrs
    tcsetattr(fileno(input), TCSAFLUSH, &attr);
  }
}

//
// do_ai_scan
//
// scan Analog Inputs
//

void do_ai_scan(comedi_t *cifh, int subdev, comedi_cmd *cmdp)
{
  int rc;
  int go;
  int quit;
  unsigned int i;
  unsigned int j;
  fd_set rdset;
  struct timeval timeout;
  unsigned int scansTot;

  go = 1;
  quit = 0;
  scansTot = 0;

  //--- data acquisition loop (can be interrupted by <control-Y>
  while(go){
    //--- setup rdset structure to wait for comedi scans and keystrokes
    FD_ZERO(&rdset);
    FD_SET(comedi_fileno(cifh),&rdset);
    FD_SET(fileno(stdin),&rdset);

    timeout.tv_sec = 1;
    timeout.tv_usec = 0;
    //--- wait untill either timeout or some data are available
    rc = select(comedi_fileno(cifh)+1,&rdset,NULL,NULL,&timeout);
    // fprintf(stderr,"select returned %d\n",rc);
    if (rc<0) {
      //--- error in select
      perror("select");
    } else if (rc==0) {
      //--- no data available before timeout
      //--- ask for available data even if driver buffer is not full
      // fprintf(stderr,"timeout, polling...\n");
      rc = comedi_poll(cifh,AI_SUBDEV);
      // fprintf(stderr,"poll returned %d\n",rc);
    } else if (FD_ISSET(fileno(stdin),&rdset)) {
      //--- standard input has some characters available
      unsigned char ch = fgetc(stdin);
      if (ch == 25 && go) {
	fprintf(stderr,"  --- <control-Y> received, quitting ... --- \n");
	quit = 1;
      } else {
	fprintf(stderr,"  --- Press <control-Y> to quit --- \n");
      }
    } else if (FD_ISSET(comedi_fileno(cifh),&rdset)) {
      //--- comedi file descriptor became ready
      // fprintf(stderr,"comedi file descriptor ready\n");
      //--- read available data
      rc = read(comedi_fileno(cifh), &buf[scansTot], sizeof(buf));
      // fprintf(stderr,"read returned %d\n",rc);
      if (rc<0) {
	if (errno==EAGAIN) {
	  //--- no data available, continue reading
	  fprintf(stderr, "Signal set but no data available, retrying\n");
	  perror("read");
	} else {
	  fprintf(stderr, "Error reading comedi device, quitting\n");
	  perror("read");
	  go = 0;
	}
      } else if (rc==0) {
	//--- if end of file reading comedi device then quit
	fprintf(stderr,"EOF, ending scan after reading %6d samples\n",
		scansTot);

	for(i=0; i<scansTot; i++) {
	  // fprintf(stderr,"%d\n",buf[i]);
	}
	for(j=0; j<scansTot; ) {
	  for(i=0; i<16; i++) {
	    if (j>=scansTot) break;
	    fprintf(stderr,"%4x ",buf[j++]);
	  }
	  fprintf(stderr,"\n");
	}

	scansTot = 0;
	go = 0;
      } else {
	//--- read available data
	fprintf(stderr,"Read %d samples\n", rc/sizeof(sampl_t));
	scansTot += rc/sizeof(sampl_t);
      }
    } else {
      /* unknown file descriptor became ready, should never happen */
      fprintf(stderr,"Unknown file descriptor ready\n");
    }
    if (quit) {
      go = 0;
    }
  }
}

//
// cmd_src
//
// decode source field of comedi command
//

char *cmd_src(int src,char *buf)
{
  buf[0]=0;
  
  if(src&TRIG_NONE)strcat(buf,"none|");
  if(src&TRIG_NOW)strcat(buf,"now|");
  if(src&TRIG_FOLLOW)strcat(buf, "follow|");
  if(src&TRIG_TIME)strcat(buf, "time|");
  if(src&TRIG_TIMER)strcat(buf, "timer|");
  if(src&TRIG_COUNT)strcat(buf, "count|");
  if(src&TRIG_EXT)strcat(buf, "ext|");
  if(src&TRIG_INT)strcat(buf, "int|");
#ifdef TRIG_OTHER
  if(src&TRIG_OTHER)strcat(buf, "other|");
#endif
  
  if(strlen(buf)==0){
    sprintf(buf,"unknown(0x%08x)",src);
  }else{
    buf[strlen(buf)-1]=0;
  }
  
  return buf;
}

//
// dump_cmd
//
// decode comedi command
//

void dump_cmd(FILE *out,comedi_cmd *cmd)
{
  char buf[100];
  
  fprintf(out,"--- begin comedi command\n");
  fprintf(out,"start:      %-8s %d\n",
	  cmd_src(cmd->start_src,buf),
	  cmd->start_arg);
  
  fprintf(out,"scan_begin: %-8s %d\n",
	  cmd_src(cmd->scan_begin_src,buf),
	  cmd->scan_begin_arg);
  
  fprintf(out,"convert:    %-8s %d\n",
	  cmd_src(cmd->convert_src,buf),
	  cmd->convert_arg);
  
  fprintf(out,"scan_end:   %-8s %d\n",
	  cmd_src(cmd->scan_end_src,buf),
	  cmd->scan_end_arg);
  
  fprintf(out,"stop:       %-8s %d\n",
	  cmd_src(cmd->stop_src,buf),
	  cmd->stop_arg);
  fprintf(out,"--- end comedi command\n");
}

